﻿using UnityEngine;
using System.Reflection;
using System.Collections;
using System.Collections.Generic;

[System.AttributeUsage(System.AttributeTargets.Field)]
public class SceneBindingAttribute : System.Attribute
{
	public SceneBindingAttribute(string name = "")
	{
		this.name = name;
	}

	public FieldInfo field;
	public string name;
}

public static class SceneBinding
{
	static IEnumerable<SceneBindingAttribute> GetSceneBindingAttributes(System.Type type)
	{
		foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
		{
			var attr = System.Attribute.GetCustomAttribute(field, typeof(SceneBindingAttribute), true) as SceneBindingAttribute;
			if (attr != null)
			{
				attr.field = field;
				if (string.IsNullOrEmpty(attr.name))
				{
					attr.name = field.Name;
				}
				yield return attr;
			}
		}
	}

	public static void Bind<T>(T target) where T : Component
	{
		Bind(target, target.gameObject);
	}

	public static void Bind<T>(T target, string path) where T : Component
	{
		Transform tf = target.transform.Find(path);
		if (tf == null)
		{
			Debug.LogError(string.Format("SceneBinding: Failed to find object '{0}' for {1}.", path, target));
			return;
		}

        Bind(target, tf.gameObject);
	}

	public static void Bind<T>(T target, GameObject context)
	{
		if (target == null)
		{
			Debug.LogWarning("Scene binding target can not be null.");
			return;
		}
		if (context == null)
		{
			Debug.LogWarning("Scene binding context can not be null.");
			return;
		}

		List<Transform> children = new List<Transform>();
		foreach (Transform child in context.transform)
		{
			children.Add(child);
		}
		children.Sort((lhs, rhs) => lhs.name.CompareTo(rhs.name));

		foreach (var attr in GetSceneBindingAttributes(typeof(T)))
		{
			System.Type ftype = attr.field.FieldType;
			string bindName = NormalizedName(attr.name, ftype.IsArray);

			if (ftype.IsArray)
			{
				System.Array array = attr.field.GetValue(target) as System.Array;
				if ((array != null) && (array.Length > 0)) continue;

				List<Transform> found = children.FindAll((lhs) => NormalizedName(lhs.name, true).Equals(bindName));
				int i = 0;
				System.Type etype = ftype.GetElementType();
				array = System.Array.CreateInstance(etype, found.Count);
				foreach (Transform child in found)
				{
					array.SetValue(GetBindingObject(child, etype), i++);
				}
				attr.field.SetValue(target, array);
			}
			else
			{
				System.Object value = attr.field.GetValue(target);
				if ((value as GameObject != null) || (value as Object != null)) continue;

				Transform found = children.Find((lhs) => NormalizedName(lhs.name, false).Equals(bindName));
				attr.field.SetValue(target, GetBindingObject(found, ftype));
			}
		}
	}

	public static void Unbind<T>(T target)
	{
		foreach (var attr in GetSceneBindingAttributes(typeof(T)))
		{
			attr.field.SetValue(target, null);
		}
	}

	static string NormalizedName(string name, bool array)
	{
		string norm = "";
		string num = "";
		foreach(char ch in name)
		{
			if (char.IsDigit(ch))
			{
				num += ch;
			}
			else
			{
				norm += num + (char.IsLetter(ch) ? ch : ' ');
				num = "";
			}
		}

		if (!array)
		{
			norm += num;
		}
		return norm.Trim();
	}

	private static System.Object GetBindingObject(Transform tran, System.Type type)
	{
		if (tran != null)
		{
			if (type.Equals(typeof(GameObject)))
			{
				return tran.gameObject;
			}
			else if (type.IsSubclassOf(typeof(Component)))
			{
				Component comp = tran.gameObject.GetComponent(type);
				if (comp == null)
				{
					Debug.LogError(string.Format("Not exist component {0} in {1}.", type.Name, tran.gameObject), tran.gameObject);
				}
				return comp;
			}
		}
		return null;
	}
}